use regex::Regex;
use rust_decimal::Decimal;
use std::str::FromStr;

#[derive(Debug, Clone, PartialEq)]
pub enum Token {
    Whitespace,
    Newline,
    Tab,
    Asterix,
    Dot,
    OpenBrace,
    CloseBrace,
    String(String),
    Ident(String),
    Decimal(Decimal),
    Unsigned(u32),
    Quote,
    DoubleQuote,
    Pic,
    Value,
    Filler,
    Redefines,
    Leading,
    Separate,
    Comp,
    Comp3,
    Occurs,
    Eof,
}

impl Eq for Token{}

impl std::fmt::Display for Token {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{:?}", self)
    }
}

impl Token {
    fn from_str(s: &str) -> Token {
        if let Some(string) = Token::try_string(s){
            string
        } else if let Some(punct) = Token::from_char(s.chars().next().unwrap()){
            punct
        } else if let Some(number) = Token::try_unsigned(s){
            number
        } else if let Some(number) = Token::try_decimal(s){
            number
        } else if let Some(keyword) = Token::try_keyword(s) {
            match keyword {
                Token::Occurs => {
                    panic!("OCCURS clase is not suppurted!");
                },
                _ =>    keyword
            }
                
        }  else {
            Token::Ident(s.to_owned())
        }
    }
    fn from_char(c: char) -> Option<Token> {
        use Token::*;
        match c {
            '.' => Some(Dot),
            ' ' => Some(Whitespace),
            '*' => Some(Asterix),
            '(' => Some(OpenBrace),
            ')' => Some(CloseBrace),
            '"' => Some(DoubleQuote),
            '\''=> Some(Quote),
            '\n'=> Some(Newline),
            '\t'=> Some(Tab),
            _   => None
        }
    }

    fn try_unsigned(s: &str) -> Option<Token> {
        Some(Token::Unsigned(s.parse::<u32>().ok()?))
    }
    fn try_decimal(s: &str) -> Option<Token>{
        let dec = Decimal::from_str(s).ok()?;
        Some(Token::Decimal(dec))
    }
    fn try_string(s: &str) -> Option<Token>{
        let mut chars = s.chars();
        let first = chars.next()?;
        if first != '\'' && first != '\"' {
            None
        } else {
            let mut result = String::new();
            while let Some(c) = chars.next(){
                if c == '\'' || c == '\"' {
                    return Some(Token::String(result))
                } else {
                    result.push(c);
                }
            }
            None
        }
    }
    
    fn try_keyword(s: &str) -> Option<Token>{
        match s.to_uppercase().as_str() {
            "VALUE"     => Some(Token::Value),
            "REDEFINES" => Some(Token::Redefines),
            "FILLER"    => Some(Token::Filler),
            "PIC"       => Some(Token::Pic),
            "COMP"      => Some(Token::Comp),
            "COMP-3"    => Some(Token::Comp3),
            "OCCURS"    => Some(Token::Occurs),
            "LEADING"   => Some(Token::Leading),
            "SEPARATE"  => Some(Token::Separate),
            _           => None,
        }
    }
}

pub fn tokenize(text: &str, drop_prefix: Option<&str>) -> Vec<(Token, u32, u32)>{
    let mut result = Vec::new();
    let preprocessed = preprocess(text, drop_prefix);
    let words = split(&preprocessed);
    for (word, col, row) in words {
        result.push((Token::from_str(&word), col, row));
    }
    result.push((Token::Eof, 0, 0));
    result
}

fn preprocess(text: &str, drop_prefix: Option<&str>) -> String {
    let linum_re = Regex::new(r"^(\d{6})?.*$").unwrap();
    let mut result = String::new();
    for line in text.lines() {
        let line = if linum_re.is_match(line) {
            let to =  if line.len() < 80 { line.len() } else { 80 };
            &line[6..to]
        } else {
            line
        };
        let line = if let Some(prefix) = drop_prefix {
            let prefix = prefix.to_owned() + "-";
            line.replace(&prefix, "")
        } else {
            line.to_owned()
        };
        result.push_str(&line);
        result.push('\n');
    }
    result
}

fn split(text: &str) -> Vec<(String, u32, u32)>{
    let mut tokens = Vec::new();
    let mut token_str = String::new();
    let mut chars = text.chars();
    let mut col = 0;
    let mut row = 0;
    while let Some(c) = chars.next() {
        col += 1;
        if let Some(punct) = Token::from_char(c){
            if !token_str.is_empty(){
                tokens.push((token_str.clone(), col, row));
                token_str.clear();
            }
            match punct {
                Token::Whitespace | Token::Tab => {},
                Token::Newline => {
                    col = 0;
                    row += 1;
                },
                Token::Asterix => {
                    while let Some(comment) = chars.next(){
                        if let Some(comment) = Token::from_char(comment) {
                            if comment == Token::Newline {
                                row += 1;
                                break;
                            }
                        }
                    }
                },
                Token::Quote | Token::DoubleQuote => {
                    let mut quoted_expr = String::new();
                    quoted_expr.push(c);
                    while let Some(qc) = chars.next(){
                        match Token::from_char(qc){
                            Some(Token::Quote) | Some(Token::DoubleQuote) => {
                                quoted_expr.push(qc);
                                break;
                            },
                            _other => quoted_expr.push(qc),
                        }
                    }
                    tokens.push((quoted_expr, col, row));
                },
                _other => {
                    tokens.push((c.to_string(), col, row))
                }
            }
        } else {
            token_str.push(c);
        }
    }
    tokens
}

